<?php

namespace App\Extends\Oss;

use DateTimeInterface;
use Exception;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use League\Flysystem\Config;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemException;
use League\Flysystem\PathPrefixer;
use League\Flysystem\Visibility;
use OSS\Core\OssException;
use OSS\Http\RequestCore_Exception;
use OSS\OssClient;

/**
 * 阿里云存储
 */
class OssAdapter implements FilesystemAdapter
{
    protected PathPrefixer $pathPrefixer;

    protected OssClient $ossClient;

    protected string $bucket;

    protected string $endPoint;

    protected ?string $cdnHost = null;

    public function __construct(protected OssConfig $config, string $bucket)
    {
        $this->pathPrefixer = new PathPrefixer('', DIRECTORY_SEPARATOR);
        $this->ossClient = new OssClient(
            $this->config->getAccessKey(),
            $this->config->getSecretKey(),
            $this->config->getEndPoint()
        );

        $this->bucket = $bucket;
        $this->endPoint = $this->config->getEndPoint();
        $this->cdnHost = $this->config->getCdnHost();
    }

    /**
     * Notes   获取文件访问地址
     *
     * @Date   : 2023/5/23 13:39
     * @Author : <Jason.C>
     * @param  string  $path
     * @return string
     * @throws Exception
     */
    public function getUrl(string $path): string
    {
        $path = $this->pathPrefixer->prefixPath($path);

        return $this->normalizeHost().ltrim($path, '/');
    }

    private function normalizeHost(): string
    {
        if ($this->cdnHost) {
            $domain = $this->cdnHost;
        } else {
            $domain = $this->bucket.'.'.$this->endPoint;
        }

        if ($this->config->getUseSsl()) {
            $domain = 'https://'.$domain;
        } else {
            $domain = 'http://'.$domain;
        }

        return rtrim($domain, '/').'/';
    }

    /**
     * Notes   : 获取文件的加密访问地址
     *
     * @Date   : 2023/5/23 13:39
     * @Author : <Jason.C>
     * @param  string  $path
     * @param  DateTimeInterface|null  $expiration
     * @param  array  $options
     * @return bool|string
     */
    public function getTemporaryUrl(
        string $path,
        DateTimeInterface $expiration = null,
        array $options = []
    ): bool|string {
        $path = $this->pathPrefixer->prefixPath($path);

        try {
            $url = $this->ossClient->signUrl(
                bucket: $this->bucket,
                object: $path,
                timeout: Carbon::now()->diffInSeconds($expiration),
                options: $options
            );

            if ($this->cdnHost) {
                $url = Str::replaceFirst($this->bucket.'.'.$this->endPoint, $this->cdnHost, $url);
            }

            return $url;
        } catch (Exception) {
            return false;
        }
    }

    /**
     * Notes   : 目录是否存在
     *
     * @Date   : 2023/5/23 13:40
     * @Author : <Jason.C>
     * @param  string  $path
     * @return bool
     */
    public function directoryExists(string $path): bool
    {
        return $this->fileExists($path);
    }

    /**
     * Notes   : 判断文件是否存在
     *
     * @Date   : 2023/5/23 13:40
     * @Author : <Jason.C>
     * @param  string  $path
     * @return bool
     */
    public function fileExists(string $path): bool
    {
        $path = $this->pathPrefixer->prefixPath($path);

        try {
            return $this->ossClient->doesObjectExist($this->bucket, $path);
        } catch (Exception) {
            return false;
        }
    }

    /**
     * Notes   : 写入文件
     *
     * @Date   : 2023/5/23 13:42
     * @Author : <Jason.C>
     * @param  string  $path
     * @param  string  $contents
     * @param  Config  $config
     * @throws Exception
     */
    public function write(string $path, string $contents, Config $config): void
    {
        $path = $this->pathPrefixer->prefixPath($path);
        $options = $config->get('options', []);

        try {
            $this->ossClient->putObject($this->bucket, $path, $contents, $options);
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92011);
        }
    }

    /**
     * @throws Exception
     */
    public function writeStream(string $path, $contents, Config $config): void
    {
        $path = $this->pathPrefixer->prefixPath($path);
        $options = $config->get('options', []);

        try {
            $this->ossClient->uploadStream($this->bucket, $path, $contents, $options);
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92011);
        }
    }

    /**
     * @throws Exception
     */
    public function read(string $path): string
    {
        $path = $this->pathPrefixer->prefixPath($path);

        try {
            return $this->ossClient->getObject($this->bucket, $path);
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage());
        }
    }

    /**
     * @throws Exception
     */
    public function readStream(string $path)
    {
        $stream = fopen('php://temp', 'w+b');

        try {
            fwrite($stream,
                $this->ossClient->getObject($this->bucket, $path,
                    [OssClient::OSS_FILE_DOWNLOAD => $stream]));
        } catch (Exception $exception) {
            fclose($stream);
            throw new Exception($exception->getMessage(), 92012);
        }
        rewind($stream);

        return $stream;
    }

    /**
     * @throws FilesystemException
     * @throws Exception
     */
    public function deleteDirectory(string $path): void
    {
        try {
            $contents = $this->listContents($path, false);
            $files = [];
            foreach ($contents as $i => $content) {
                if ($content instanceof DirectoryAttributes) {
                    $this->deleteDirectory($content->path());
                    continue;
                }
                $files[] = $this->pathPrefixer->prefixPath($content->path());
                if ($i && 0 == $i % 100) {
                    $this->ossClient->deleteObjects($this->bucket, $files);
                    $files = [];
                }
            }
            !empty($files) && $this->ossClient->deleteObjects($this->bucket, $files);
            $this->ossClient->deleteObject($this->bucket, $this->pathPrefixer->prefixDirectoryPath($path));
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92013);
        }
    }

    /**
     * @throws FilesystemException
     * @throws Exception
     */
    public function listContents(string $path, bool $deep): iterable
    {
        $directory = $this->pathPrefixer->prefixDirectoryPath($path);
        $nextMarker = '';
        while (true) {
            $options = [
                OssClient::OSS_PREFIX => $directory,
                OssClient::OSS_MARKER => $nextMarker,
            ];

            try {
                $listObjectInfo = $this->ossClient->listObjects($this->bucket, $options);
                $nextMarker = $listObjectInfo->getNextMarker();
            } catch (Exception $exception) {
                throw new Exception($exception->getMessage());
            }

            $prefixList = $listObjectInfo->getPrefixList();
            foreach ($prefixList as $prefixInfo) {
                $subPath = $this->pathPrefixer->stripDirectoryPrefix($prefixInfo->getPrefix());
                if ($subPath == $path) {
                    continue;
                }
                yield new DirectoryAttributes($subPath);
                if (true === $deep) {
                    $contents = $this->listContents($subPath, true);
                    foreach ($contents as $content) {
                        yield $content;
                    }
                }
            }

            $listObject = $listObjectInfo->getObjectList();
            if (!empty($listObject)) {
                foreach ($listObject as $objectInfo) {
                    $objectPath = $this->pathPrefixer->stripPrefix($objectInfo->getKey());
                    $objectLastModified = strtotime($objectInfo->getLastModified());
                    if (str_ends_with($objectPath, '/')) {
                        continue;
                    }
                    yield new FileAttributes($objectPath, $objectInfo->getSize(), null, $objectLastModified);
                }
            }

            if ('true' !== $listObjectInfo->getIsTruncated()) {
                break;
            }
        }
    }

    /**
     * @throws Exception
     */
    public function createDirectory(string $path, Config $config): void
    {
        try {
            $this->ossClient->createObjectDir($this->bucket, $this->pathPrefixer->prefixPath($path));
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92014);
        }
    }

    /**
     * @throws Exception
     */
    public function setVisibility(string $path, string $visibility): void
    {
        $object = $this->pathPrefixer->prefixPath($path);

        $acl = Visibility::PUBLIC === $visibility ? OssClient::OSS_ACL_TYPE_PUBLIC_READ : OssClient::OSS_ACL_TYPE_PRIVATE;

        try {
            $this->ossClient->putObjectAcl($this->bucket, $object, $acl);
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92015);
        }
    }

    /**
     * @throws Exception
     */
    public function visibility(string $path): FileAttributes
    {
        $meta = $this->getMetadata($path);
        if (null === $meta->visibility()) {
            throw new Exception('visibility', 92016);
        }

        return $meta;
    }

    /**
     * Notes   : 获取文件元数据
     *
     * @Date   : 2023/5/23 13:40
     * @Author : <Jason.C>
     * @param  string  $path
     * @return FileAttributes
     * @throws RequestCore_Exception
     * @throws OssException
     */
    protected function getMetadata(string $path): FileAttributes
    {
        $result = $this->ossClient->getObjectMeta($this->bucket, $this->pathPrefixer->prefixPath($path));

        $size = isset($result['content-length']) ? intval($result['content-length']) : 0;
        $timestamp = isset($result['last-modified']) ? strtotime($result['last-modified']) : 0;
        $mimetype = $result['content-type'] ?? '';

        try {
            $acl = $this->ossClient->getObjectAcl($this->bucket, $this->pathPrefixer->prefixPath($path), []);
            $visibility = OssClient::OSS_ACL_TYPE_PRIVATE === $acl ? Visibility::PRIVATE : Visibility::PUBLIC;
        } catch (OssException) {
            $visibility = Visibility::PUBLIC;
        }

        return new FileAttributes($path, $size, $visibility, $timestamp, $mimetype);
    }

    /**
     * @throws Exception
     */
    public function mimeType(string $path): FileAttributes
    {
        $meta = $this->getMetadata($path);
        if (null === $meta->mimeType()) {
            throw new Exception('visibility', 92016);
        }

        return $meta;
    }

    /**
     * @throws Exception
     */
    public function lastModified(string $path): FileAttributes
    {
        $meta = $this->getMetadata($path);
        if (null === $meta->lastModified()) {
            throw new Exception('visibility', 92016);
        }

        return $meta;
    }

    /**
     * @throws Exception
     */
    public function fileSize(string $path): FileAttributes
    {
        $meta = $this->getMetadata($path);
        if (null === $meta->fileSize()) {
            throw new Exception('visibility', 92016);
        }

        return $meta;
    }

    /**
     * @throws Exception
     */
    public function move(string $source, string $destination, Config $config): void
    {
        try {
            $this->copy($source, $destination, $config);
            $this->delete($source);
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92018);
        }
    }

    /**
     * @throws Exception
     */
    public function copy(string $source, string $destination, Config $config): void
    {
        $path = $this->pathPrefixer->prefixPath($source);
        $newPath = $this->pathPrefixer->prefixPath($destination);

        try {
            $this->ossClient->copyObject($this->bucket, $path, $this->bucket, $newPath);
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92019);
        }
    }

    /**
     * @throws Exception
     */
    public function delete(string $path): void
    {
        $path = $this->pathPrefixer->prefixPath($path);

        try {
            $this->ossClient->deleteObject($this->bucket, $path);
        } catch (Exception $exception) {
            throw new Exception($exception->getMessage(), 92013);
        }
    }
}
